﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

namespace SmartMathLibrary.ExpressionEvaluation
{
    /// <summary>
    /// This class provides the evaluation of a polish reverse expression like 5 1 2 + 4 * + 3 − sin. It supports the operators 
    /// +, -, *, /, %, sin, cos, tan, sinh, cosh, tanh, asin, acos, atan, ln, log, sqrt, exp, floor, ceiling, 
    /// truncate, pow and atan2.
    /// </summary>
    [Serializable]
    public class PolishReverseNotationExpressionEvaluator : AbstractExpressionEvaluator
    {
        /// <summary>
        /// The stack for the evaluation.
        /// </summary>
        private readonly Stack<Double> stack;

        /// <summary>
        /// The parser which holds the expression to evaluate.
        /// </summary>
        private PolishReverseNotationParser parser;

        /// <summary>
        /// Initializes a new instance of the <see cref="PolishReverseNotationExpressionEvaluator"/> class.
        /// </summary>
        /// <param name="parser">The parser which holds the expression to evaluate.</param>
        public PolishReverseNotationExpressionEvaluator(PolishReverseNotationParser parser)
        {
            if (parser == (PolishReverseNotationParser) null)
            {
                throw new ArgumentNullException("parser");
            }

            this.parser = parser;
            this.stack = new Stack<double>();
        }

        /// <summary>
        /// Gets or sets the parser which holds the expression to evaluate.
        /// </summary>
        /// <value>The parser which holds the expression to evaluate.</value>
        public PolishReverseNotationParser Parser
        {
            get { return parser; }
            set { parser = value; }
        }

        /// <summary>
        /// Evaluates the specified mathematical term.
        /// </summary>
        /// <returns>The result of the mathematical term.</returns>
        public override double Evaluate()
        {
            this.parser.Reset();

            bool runParsing = true;

            while (runParsing)
            {
                runParsing = this.parser.HasMoreTokens;

                if (OperatorInformation.IsNumber(this.parser.CurrentToken()))
                {
                    this.stack.Push(Double.Parse(this.parser.CurrentToken(), NumberStyles.Float,
                                                 NumberFormatInfo.InvariantInfo));
                }
                else if (OperatorInformation.IsOperator(this.parser.CurrentToken()))
                {
                    if (this.stack.Count < 2)
                    {
                        throw new NotationSyntaxErrorException(
                            "The specified expression to evaluate has a syntax error.");
                    }

                    double secondValue = this.stack.Pop();
                    double firstValue = this.stack.Pop();

                    if (this.parser.CurrentToken().Equals("+"))
                    {
                        this.stack.Push(firstValue + secondValue);
                    }
                    else if (this.parser.CurrentToken().Equals("-"))
                    {
                        this.stack.Push(firstValue - secondValue);
                    }
                    else if (this.parser.CurrentToken().Equals("*"))
                    {
                        this.stack.Push(firstValue * secondValue);
                    }
                    else if (this.parser.CurrentToken().Equals("/"))
                    {
                        this.stack.Push(firstValue / secondValue);
                    }
                    else if (this.parser.CurrentToken().Equals("%"))
                    {
                        this.stack.Push(firstValue % secondValue);
                    }
                }
                else if (OperatorInformation.IsOneParameterFunction(this.parser.CurrentToken()))
                {
                    if (this.stack.Count < 1)
                    {
                        throw new NotationSyntaxErrorException(
                            "The specified expression to evaluate has a syntax error.");
                    }

                    if (this.parser.CurrentToken().Equals("sin"))
                    {
                        this.stack.Push(Math.Sin(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("cos"))
                    {
                        this.stack.Push(Math.Cos(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("tan"))
                    {
                        this.stack.Push(Math.Tan(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("sinh"))
                    {
                        this.stack.Push(Math.Sinh(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("cosh"))
                    {
                        this.stack.Push(Math.Cosh(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("tanh"))
                    {
                        this.stack.Push(Math.Tanh(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("asin"))
                    {
                        this.stack.Push(Math.Asin(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("acos"))
                    {
                        this.stack.Push(Math.Acos(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("atan"))
                    {
                        this.stack.Push(Math.Atan(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("ln"))
                    {
                        this.stack.Push(Math.Log(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("log"))
                    {
                        this.stack.Push(Math.Log10(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("sqrt"))
                    {
                        this.stack.Push(Math.Sqrt(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("exp"))
                    {
                        this.stack.Push(Math.Exp(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("floor"))
                    {
                        this.stack.Push(Math.Floor(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("ceiling"))
                    {
                        this.stack.Push(Math.Ceiling(this.stack.Pop()));
                    }
                    else if (this.parser.CurrentToken().Equals("truncate"))
                    {
                        this.stack.Push(Math.Truncate(this.stack.Pop()));
                    }
                }
                else if (OperatorInformation.IsTwoParameterFunction(this.parser.CurrentToken()))
                {
                    if (this.stack.Count < 2)
                    {
                        throw new NotationSyntaxErrorException(
                            "The specified expression to evaluate has a syntax error.");
                    }

                    double secondValue = this.stack.Pop();
                    double firstValue = this.stack.Pop();

                    if (this.parser.CurrentToken().Equals("pow"))
                    {
                        this.stack.Push(Math.Pow(firstValue, secondValue));
                    }
                    else if (this.parser.CurrentToken().Equals("atan2"))
                    {
                        this.stack.Push(Math.Atan2(firstValue, secondValue));
                    }
                }
                else
                {
                    throw new OperatorNotSupportedException("The operator " + this.parser.CurrentToken() +
                                                            " will not be supported");
                }

                this.parser.ShiftRight();
            }

            if (this.stack.Count != 1)
            {
                throw new NotationSyntaxErrorException("The specified expression to evaluate has a syntax error.");
            }

            return this.stack.Pop();
        }
    }
}